package net.frenopatico.citadels.dao.impl;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import javax.persistence.Embedded;
import javax.persistence.Transient;

import lombok.extern.java.Log;
import net.frenopatico.citadels.dao.IGenericDAO;

import com.google.appengine.api.datastore.EntityNotFoundException;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Query;
import com.googlecode.objectify.util.DAOBase;

/**
 * Base implementation for DAOs.
 *
 * @author eduardo.ramirez.ronco@gmail.com
 *
 * @param <T>
 */
@Log
public abstract class AbstractGenericDAO<T> extends DAOBase implements IGenericDAO<T> { // NOPMD

    private static final int BAD_MODIFIERS = Modifier.FINAL | Modifier.STATIC | Modifier.TRANSIENT;

    private final transient Class<T> clazz;

    /**
     * Obtain the domain class.
     */
    @SuppressWarnings( "unchecked" )
    public AbstractGenericDAO() {
        super();
        clazz = ( (Class<T>) ( (ParameterizedType) getClass().getGenericSuperclass() ).getActualTypeArguments()[0] );
    }

    /**
     * We've got to get the associated domain class somehow
     *
     * @param classType Class for the generic type.
     */
    protected AbstractGenericDAO( final Class<T> classType ) {
        super();
        this.clazz = classType;
    }

    /**
     * {@inheritDoc}
     */
    public final Key<T> put( final T entity ) {
        putEx( entity );
        return ofy().put( entity );
    }

    /**
     * Method to extend functionality of method put.
     *
     * @param entity Entity to persist in the context.
     */
    protected abstract void putEx( final T entity );

    /**
     * {@inheritDoc}
     */
    public Map<Key<T>, T> put( final Iterable<T> entities ) {
        putEx( entities );
        return ofy().put( entities );
    }

    /**
     * Method to extend functionality of method put
     *
     * @param entities Entities to persist.
     */
    protected abstract void putEx( final Iterable<T> entities );

    /**
     * {@inheritDoc}
     */
    public void delete( final T entity ) {
        deleteEx( entity );
        ofy().delete( entity );
    }

    /**
     * Method to extend functionality of method delete.
     *
     * @param entity Entity to delete.
     */
    protected abstract void deleteEx( final T entity );

    /**
     * {@inheritDoc}
     */
    public void delete( final Key<T> entityKey ) {
        deleteEx( entityKey );
        ofy().delete( entityKey );
    }

    /**
     * Method to extend functionality of method delete.
     *
     * @param entityKey Key of the Entity to delete.
     */
    protected abstract void deleteEx( final Key<T> entityKey );

    /**
     * {@inheritDoc}
     */
    public void delete( final Iterable<T> entities ) {
        deleteEx( entities );
        ofy().delete( entities );
    }

    /**
     * Method to extend functionality of method delete.
     *
     * @param entities Entities to delete.
     */
    protected abstract void deleteEx( final Iterable<T> entities );

    /**
     * {@inheritDoc}
     */
    public void deleteKeys( final Iterable<Key<T>> keys ) {
        deleteKeysEx( keys );
        ofy().delete( keys );
    }

    /**
     * Method to extend functionality of method deleteKeys.
     *
     * @param keys Keys of the entities to delete.
     */
    protected abstract void deleteKeysEx( final Iterable<Key<T>> keys );

    /**
     * {@inheritDoc}
     */
    public T get( final Long entityId ) throws EntityNotFoundException {
        final T entity = ofy().get( this.clazz, entityId );
        getEx( entityId, entity );
        return entity;
    }

    /**
     * Method to extend functionality of method get.
     *
     * @param entityId Id of the entity to delete.
     * @param entity Entity to modify.
     */
    protected abstract void getEx( final Long entityId, final T entity );

    /**
     * {@inheritDoc}
     */
    public T get( final String entityId ) throws EntityNotFoundException {
        final T entity = ofy().get( this.clazz, entityId );
        getEx( entityId, entity );
        return entity;
    }

    /**
     * Method to extend functionality of method get.
     *
     * @param entityId Id of the entity to delete.
     * @param entity Entity to modify.
     */
    protected abstract void getEx( final String entityId, final T entity );

    /**
     * {@inheritDoc}
     */
    public T get( final Key<T> key ) throws EntityNotFoundException {
        final T entity = ofy().get( key );
        getEx( key, entity );
        return entity;
    }

    /**
     * Method to extend functionality of method get.
     *
     * @param key Key of the entity to look up in the context.
     * @param entity Entity to modify.
     */
    protected abstract void getEx( final Key<T> key, T entity );

    /**
     * {@inheritDoc}
     */
    public Key<T> getKey( final Long entityId ) {
        return new Key<T>( clazz, entityId );
    }

    /**
     * {@inheritDoc}
     */
    public List<T> listAll() {
        final Query<T> query = getQuery();
        final List<T> entities = query.list();
        listAllEx( entities );
        return entities;
    }

    /**
     * Method to extend functionality of method listAll.
     *
     * @param entities All the entities in the context to modify.
     */
    protected abstract void listAllEx( List<T> entities );

    /**
     * {@inheritDoc}
     */
    public T getByExample( final T exampleEntity ) {
        final Query<T> queryByExample = buildQueryByExample( exampleEntity );
        final List<Key<T>> keys = queryByExample.listKeys();

        T entity;
        if( keys.size() > 1 ) {
            throw new IllegalStateException( "Too many results" );
        } else {
            try {
                entity = get( keys.get( 0 ) ); // NOPMD
            } catch( EntityNotFoundException e ) {
                throw new IllegalStateException( "Imposible situation." );
            }
        }

        getByExampleEx( exampleEntity, entity );

        return entity;
    }

    /**
     * Method to extend functionality of method getByExample.
     *
     * @param exampleEntity Entity to match the non null fields against.
     * @param entity Entity to return.
     */
    protected abstract void getByExampleEx( final T exampleEntity, final T entity );

    /**
     * {@inheritDoc}
     */
    public List<T> listByExample( final T exampleEntity ) {
        final Query<T> queryByExample = buildQueryByExample( exampleEntity );
        final List<T> entities = queryByExample.list();

        listByExampleEx( exampleEntity, entities );

        return entities;
    }

    /**
     * Method to extend functionality of method listByExample.
     *
     * @param exampleEntity Entity to match the non null fields against.
     * @param entities Entities to return.
     */
    protected abstract void listByExampleEx( final T exampleEntity, final List<T> entities );

    /**
     * Build a query from the non null entities in the example.
     *
     * @param exampleEntity Example entity.
     * @return Query to search for the non null fields.
     */
    private Query<T> buildQueryByExample( final T exampleEntity ) {
        final Query<T> query = getQuery(); // NOPMD

        int numFields = 0; // NOPMD

        // Add all non-null properties to query filter
        for( final Field field : clazz.getDeclaredFields() ) {
            if( ignore( field ) ) {
                continue;
            }

            field.setAccessible( true );

            Object value;
            try {
                value = field.get( exampleEntity );
                if( value != null ) {
                    numFields = numFields + 1;
                    query.filter( field.getName(), value );
                }
            } catch( IllegalArgumentException e ) {
                log.finest( "Illegal argument in: " + field.getName() );
            } catch( IllegalAccessException e ) {
                log.finest( "Illegal access in: " + field.getName() );
            }
        }

        if( numFields == 0 ) {
            throw new IllegalArgumentException( "All fields are null." );
        }

        return query;
    }

    /**
     * Get a query for this Entity.
     *
     * @return Query for the entity.
     */
    protected Query<T> getQuery() {
        return ofy().query( clazz );
    }

    /**
     * Ignore transient, embedded, array, and collection properties.
     *
     * @param field Field to test.
     * @return True if field is to be ignored, False otherwise.
     */
    private boolean ignore( final Field field ) {
        return field.isAnnotationPresent( Transient.class ) || ( field.isAnnotationPresent( Embedded.class ) ) || ( field.getType().isArray() ) || ( Collection.class.isAssignableFrom( field.getType() ) ) || ( ( field.getModifiers() & BAD_MODIFIERS ) != 0 );
    }
}
